/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.persistence.hibernate;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.*;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import org.hibernate.dialect.HSQLDialect;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.xptracker.persistence.*;
import org.xptracker.services.EngineReceiver;
import org.xptracker.services.JsEngine;
import org.xptracker.util.ThreadLocalCleaner;

import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.*;

@Singleton
public class HibernatePersistenceManager implements PersistenceManager {
  private Log log = LogFactory.getLog(getClass());
  private ThreadLocal<Session> currentSession = ThreadLocalCleaner.registerForCleanup(new ThreadLocal<Session>());

  private static final Map<Class, DatabaseMaintainer> DB_MAINTAINERS = new HashMap<Class, DatabaseMaintainer>() {{
    put(HSQLDialect.class, new HSQLDatabaseMaintainer());
  }};

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private JsEngine jsEngine;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-dialect")
  private Class dialectClass;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-driver")
  private Class driverClass;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-url")
  private String url;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-username")
  private String username;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-password")
  private String password;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  @Named("hibernate-ddl-mode")
  private String ddlMode;

  private SessionFactory factory;
  private PersistenceListener listener;

  private static final Map<Class, ParameterBinder> PARAMETER_BINDERS = getParameterBinders();

  public String getDbType() {
    return "hibernate";
  }

  @Inject
  public HibernatePersistenceManager(AggregatePersistenceListeners listener) {
    this.listener = listener;
  }

  public void initialize(DomainRegistry domainRegistry) {
    log.info("Initializing Hibernate...");
    AnnotationConfiguration config = new AnnotationConfiguration();
    for (Class c : domainRegistry.getDomainClasses()) {
      config.addAnnotatedClass(c);
    }
    config.addAnnotatedClass(HibernateObjectChange.class);

    config.setProperty("hibernate.dialect", dialectClass.getName()).
        setProperty("hibernate.connection.driver_class", driverClass.getName()).
        setProperty("hibernate.connection.url", url).
        setProperty("hibernate.connection.username", username).
        setProperty("hibernate.connection.password", password).
        setProperty("hibernate.connection.pool_size", "1").
        setProperty("hibernate.connection.autocommit", "false").
        setProperty("hibernate.cache.provider_class", WhirlyCacheProvider.class.getName()).
        setProperty("hibernate.hbm2ddl.auto", ddlMode).
        setProperty("hibernate.max_fetch_depth", "3").
        setProperty("hibernate.jdbc.batch_size", "0");
    factory = config.buildSessionFactory();
  }

  public void shutdown() {
    log.info("Shutting down Hibernate...");
    factory.close();
  }

  public void doMaintenance() {
    final DatabaseMaintainer maintainer = DB_MAINTAINERS.get(dialectClass);
    if (maintainer != null) {
      log.info("Performing database maintenance.");
      withSession(new SessionReceiver<Object>() {
        public Object receiveSession(Session session) {
          try {
            maintainer.doMaintenance(session);
          } catch (SQLException e) {
            throw new RuntimeException(e);
          }
          return null;
        }
      });
    }
  }

  public <T> T inTransaction(final UpdateHandler<T> handler) {
    return withSession(new SessionReceiver<T>() {
      public T receiveSession(final Session session) {
        boolean newTransaction = !session.getTransaction().isActive();
        try {
          if (newTransaction) {
            session.beginTransaction();
          }
          T result = handler.doUpdate(new UpdateReceiver() {
            public void insert(Object o) {
              listener.beforeInsert(o);
              session.save(o);
              listener.afterInsert(o);
            }

            public void update(Object o) {
              listener.beforeUpdate(o);
              session.update(o);
              listener.afterUpdate(o);
            }

            public void remove(Object o) {
              listener.onDelete(o);
              session.delete(o);
            }
          });
          if (newTransaction) {
            session.getTransaction().commit();
          }
          listener.onCommit();
          return result;
        } catch (Throwable t) {
          if (newTransaction) {
            try {
              session.getTransaction().rollback();
              listener.onRollback();
            } catch (HibernateException e) {
              log.error("Error rolling back", e);
            }
          }
          throw new PersistenceFailureException(t);
        }
      }
    });
  }

  private <T> T withSession(SessionReceiver<T> sessionReceiver) {
    boolean useExistingSession = currentSession.get() != null;
    final Session session = useExistingSession ? currentSession.get() : factory.openSession();
    if (!useExistingSession) {
      currentSession.set(session);
    }

    try {
      return sessionReceiver.receiveSession(session);
    } finally {
      if (!useExistingSession) {
        session.close();
        currentSession.remove();
      }
    }
  }

  public <T> List<T> query(final String query) {
    return withSession(new SessionReceiver<List<T>>() {
      @SuppressWarnings({"unchecked"})
      public List<T> receiveSession(Session session) {
        return session.createQuery(query).list();
      }
    });
  }

  public <T> List<T> query(final String queryStr, final Map<String, Object> args) {
    return withSession(new SessionReceiver<List<T>>() {
      @SuppressWarnings({"unchecked"})
      public List<T> receiveSession(Session session) {
        Query query = session.createQuery(queryStr);
        for(String key: args.keySet()) {
          Object value = args.get(key);
          ParameterBinder binder = getBinder(value);
          binder.bind(query, key, value);
        }
        return query.list();
      }
    });
  }

  private ParameterBinder getBinder(Object value) {
    return PARAMETER_BINDERS.get(value.getClass());
  }

  public <T> List<T> queryWithScript(final String query, final Object... args) {
    return withSession(new SessionReceiver<List<T>>() {
      @SuppressWarnings({"unchecked"})
      public List<T> receiveSession(final Session session) {
        final List<T> results = new ArrayList<T>();
        jsEngine.withEngine(false, new EngineReceiver() {
          public void receive(Context cx, Scriptable scope) {
            scope.put("session", scope, session);
            scope.put("args", scope, args);
            scope.put("results", scope, results);
            scope.put("restrict", scope, new Restricter());
            try {
              cx.evaluateReader(scope,
                new InputStreamReader(getClass().getClassLoader().getResourceAsStream(query)),
                  "<" + query + ">", 1, null);
            } catch (IOException e) {
              throw new RuntimeException(e);
            }
          }
        });
        return results;
      }
    });
  }

  public static Map<Class, ParameterBinder> getParameterBinders() {
    Map<Class, ParameterBinder> binders = new HashMap<Class, ParameterBinder>();
    binders.put(String.class, new ParameterBinder<String>() {
      public void bind(Query query, String key, String value) {
        query.setString(key, value);
      }
    });

    binders.put(Long.class, new ParameterBinder<Long>() {
      public void bind(Query query, String key, Long value) {
        query.setLong(key, value);
      }
    });

    binders.put(Date.class, new ParameterBinder<Date>() {
      public void bind(Query query, String key, Date value) {
        query.setDate(key, value);
      }
    });

    binders.put(java.sql.Date.class, new ParameterBinder<java.sql.Date>() {
      public void bind(Query query, String key, java.sql.Date value) {
        query.setDate(key, new Date(value.getTime()));
      }
    });

    binders.put(java.sql.Timestamp.class, new ParameterBinder<Timestamp>() {
      public void bind(Query query, String key, Timestamp value) {
        query.setDate(key, new Date(value.getTime()));
      }
    });
    return binders;
  }

  public static class Restricter {
    public Criterion _in(String fieldName, Object[] objects) {
      return Restrictions.in(fieldName, objects);
    }

    public Criterion _in(String fieldName, Collection objects) {
      return Restrictions.in(fieldName, objects);
    }

    public Criterion gt(String fieldName, Object object) {
      return Restrictions.gt(fieldName, object);
    }

    public Criterion lt(String fieldName, Object object) {
      return Restrictions.lt(fieldName, object);
    }

    public Criterion or(Criterion first, Criterion second) {
      return Restrictions.or(first, second);
    }

    public Criterion isNull(String fieldName) {
      return Restrictions.isNull(fieldName);
    }
  }

  public void flush() {
    withSession(new SessionReceiver<Object>() {
      public Object receiveSession(Session session) {
        session.flush();
        return null;
      }
    });
  }

  public <T extends Identified> List<T> listObjects(final Class<T> objectClass, final Set<String> ids) {
    if (ids == null || ids.isEmpty()) {
      return Collections.emptyList();
    }
    
    final Set<Long> actualIds = new LinkedHashSet<Long>();
    for(String idstr: ids) {
      actualIds.add(new Long(idstr));
    }

    return withSession(new SessionReceiver<List<T>>() {
      @SuppressWarnings({"unchecked"})
      public List<T> receiveSession(Session session) {
        Criteria c = session.createCriteria(objectClass);
        c.add(Restrictions.in("id", actualIds));
        return c.list();
      }
    });
  }

  public <T> T loadObject(final Class<? extends T> entityType, final String id) {
    return loadObject(entityType, id, null);
  }

  public <T> T loadObject(final Class<? extends T> entityType, final String id, final FetchHelper<T> helper) {
    return withSession(new SessionReceiver<T>() {
      @SuppressWarnings({"unchecked"})
      public T receiveSession(Session session) {
        try {
          T t = (T) session.load(entityType, new Long(id));
          Hibernate.initialize(t);
          if (helper != null) {
            helper.fetch(t);
          }
          return t;
        } catch (ObjectNotFoundException e) {
          return null;
        }
      }
    });
  }

  public static interface SessionReceiver<T> {
    T receiveSession(Session session);
  }

  public static interface DatabaseMaintainer {
    void doMaintenance(Session session) throws SQLException;
  }
}
